package top.codedance.iotp.client.plugins.sync.bacnet;

import com.alibaba.fastjson.JSONArray;
import com.alibaba.fastjson.JSONObject;
import top.codedance.iotp.client.ClientApplication;
import top.codedance.iotp.client.net.listenner.PluginService;
import top.codedance.iotp.common.entity.PluginResultEntity;
import com.serotonin.bacnet4j.LocalDevice;
import com.serotonin.bacnet4j.RemoteDevice;
import com.serotonin.bacnet4j.exception.BACnetException;
import com.serotonin.bacnet4j.npdu.ip.IpNetwork;
import com.serotonin.bacnet4j.npdu.ip.IpNetworkBuilder;
import com.serotonin.bacnet4j.transport.DefaultTransport;
import com.serotonin.bacnet4j.type.Encodable;
import com.serotonin.bacnet4j.type.EncodedValue;
import com.serotonin.bacnet4j.type.enumerated.ObjectType;
import com.serotonin.bacnet4j.type.enumerated.PropertyIdentifier;
import com.serotonin.bacnet4j.type.primitive.Boolean;
import com.serotonin.bacnet4j.type.primitive.ObjectIdentifier;
import com.serotonin.bacnet4j.type.primitive.Real;
import com.serotonin.bacnet4j.type.primitive.SignedInteger;
import com.serotonin.bacnet4j.util.PropertyReferences;
import com.serotonin.bacnet4j.util.PropertyValues;
import com.serotonin.bacnet4j.util.ReadListener;
import com.serotonin.bacnet4j.util.RequestUtils;
import org.apache.log4j.Logger;
import org.ini4j.Ini;
import org.ini4j.Wini;

import java.math.BigDecimal;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
import java.util.stream.Collectors;

public class BacNetIpService implements PluginService, Runnable {
    private static Logger logger = ClientApplication.getLogger(BacNetIpService.class);
    private static Map<Integer, LocalDevice> localDeviceMap = new HashMap<>();
    private static BacNetIpService self = null;
    private static int LoadDeviceId = 2001236;
    private Integer clientId;
    private Wini ini;
    private volatile CountDownLatch cdl = new CountDownLatch(1);

    private BacNetIpService(Integer clientId, Wini ini){
        this.clientId = clientId;
        this.ini = ini;
    }

    public static PluginService getInstance(Integer clientId, Wini ini) {
        synchronized (BacNetIpService.class) {
            if(self == null) {
                synchronized (BacNetIpService.class) {
                    BacNetIpService bacNetIpService = new BacNetIpService(clientId, ini);
                    BacNetExecuterImpl.pluginServiceWorkerPool.submit(bacNetIpService);
                    self = bacNetIpService;
                }
            }
        }
        return self;
    }

    @Override
    public void run() {
        init();
        logger.debug("BacNetIp is Running.");
    }

    @Override
    public void init() {
        try {
            String localBindAddress, subnetAddress;
            int networkPrefixLength, port;
            if (ini != null && ini.containsKey("bacnet")) {
                Ini.Section section = ini.get("bacnet");
                if (section.containsKey("localBindAddress." + this.clientId)) {
                    localBindAddress = section.get("localBindAddress." + this.clientId);
                } else {
                    throw new Exception("parameter localBindAddress is empty.");
                }
                if (section.containsKey("subnetAddress." + this.clientId)) {
                    subnetAddress = section.get("subnetAddress." + this.clientId);
                } else {
                    throw new Exception("parameter subnetAddress is empty.");
                }
                if (section.containsKey("networkPrefixLength." + this.clientId)) {
                    networkPrefixLength = Integer.valueOf(section.get("networkPrefixLength." + this.clientId)).intValue();
                } else {
                    throw new Exception("parameter networkPrefixLength is empty.");
                }
                if (section.containsKey("port." + this.clientId)) {
                    port = Integer.valueOf(section.get("port." + this.clientId)).intValue();
                } else {
                    throw new Exception("parameter port is empty.");
                }
            } else {
                throw new Exception("bacnet_service parameters is empty.");
            }
            //创建网络对象
            IpNetwork ipNetwork = new IpNetworkBuilder()
                    //本机的ip
                    .withLocalBindAddress(localBindAddress)
                    //掩码和长度，如果不知道本机的掩码和长度的话，可以使用代码的工具类IpNetworkUtils获取
                    .withSubnet(subnetAddress, networkPrefixLength)
                    //默认的UDP端口
                    .withPort(port)
                    .withReuseAddress(true)
                    .build();
            //创建虚拟的本地设备，随意设置一个deviceId 设备的唯一标识
            LocalDevice localDevice = new LocalDevice(LoadDeviceId++, new DefaultTransport(ipNetwork));
            //初始化本地设备
            localDevice.initialize();
            //搜寻网段内远程设备
            localDevice.startRemoteDeviceDiscovery();
            localDeviceMap.put(this.clientId, localDevice);
        } catch (Exception e){
            logger.info("BACNet local device initialize fail.");
            logger.debug(e.getMessage());
        } finally {
            cdl.countDown();
        }
    }

    private void waiting(){
        try {
            cdl.await(3, TimeUnit.SECONDS);
        }catch (Exception e){}
    }

    @Override
    public PluginResultEntity execute(JSONArray items) {
        waiting();
        PluginResultEntity pluginResultEntity = new PluginResultEntity();
        try {
            JSONArray readsArray = new JSONArray();
            JSONArray writesArray = new JSONArray();
            JSONObject joTmp;
            for (int i = 0;i < items.size();i++){
                joTmp = items.getJSONObject(i);
                if(joTmp.containsKey("isWrite") && joTmp.getBoolean("isWrite")){
                    writesArray.add(joTmp);
                }else{
                    readsArray.add(joTmp);
                }
            }
            JSONObject results = new JSONObject();
            if(readsArray.size() > 0) {
                reads(results, readsArray);
            }
            if(writesArray.size() > 0){
                writes(results, writesArray);
            }
            pluginResultEntity.setContext(results);
            pluginResultEntity.setOk(true);
        } catch (Exception e){
            pluginResultEntity.setOk(false);
            pluginResultEntity.setMsg(e.getMessage());
            logger.debug(e.getMessage());
        }
        return pluginResultEntity;
    }

    private void reads(JSONObject results, JSONArray items){
        LocalDevice localDevice = localDeviceMap.get(this.clientId);
        Map<Integer, JSONArray> groupItems = new HashMap<>();
        JSONObject joTmp;
        JSONArray jaTmp;
        for (int i = 0;i < items.size();i++){
            joTmp = items.getJSONObject(i);
            if(groupItems.containsKey(joTmp.getInteger("deviceId"))){
                jaTmp = groupItems.get(joTmp.getInteger("deviceId"));
                jaTmp.add(joTmp);
            }else{
                jaTmp = new JSONArray();
                jaTmp.add(joTmp);
                groupItems.put(joTmp.getInteger("deviceId"), jaTmp);
            }
        }
        Set<Integer> deviceIds = groupItems.keySet();
        for (Integer deviceId : deviceIds) {
            try {
                //获取某个远程设备
                RemoteDevice remoteDevice = localDevice.getRemoteDeviceBlocking(deviceId);
                //获取远程设备的标识符对象
                List<ObjectIdentifier> objectList = RequestUtils.getObjectList(localDevice, remoteDevice).getValues();
                JSONArray currentItems = groupItems.get(deviceId);
                for (int i = 0; i < currentItems.size(); i++) {
                    try {
                        final JSONObject currentJo = currentItems.getJSONObject(i);
                        List<ObjectIdentifier> filter = objectList.stream().filter(e -> e.getObjectType().equals(ObjectType.forId(currentJo.getIntValue("datatype"))) && e.getInstanceNumber() == currentJo.getIntValue("instance")).collect(Collectors.toList());
                        PropertyValues values = readValueByProperty(localDevice, remoteDevice, filter, null, PropertyIdentifier.forId(currentJo.getIntValue("property")));
                        Encodable property = values.get(filter.get(0), PropertyIdentifier.forId(currentJo.getIntValue("property")));
                        results.put(currentJo.getString("tag"), property.toString());
                    }catch (Exception ex){
                        logger.debug(ex.getMessage());
                    }
                }
            }catch (Exception e){
                logger.debug(e.getMessage());
            }
        }
    }

    private static PropertyValues readValueByProperty(final LocalDevice localDevice, final RemoteDevice d, final List<ObjectIdentifier> ois, final ReadListener callback, PropertyIdentifier propertyIdentifier) throws BACnetException {
        if (ois.size() == 0) {
            return new PropertyValues();
        }
        final PropertyReferences refs = new PropertyReferences();
        for (final ObjectIdentifier oid : ois) {
            refs.add(oid, propertyIdentifier);
        }
        return RequestUtils.readProperties(localDevice, d, refs, false, callback);
    }

    private void writes(JSONObject results, JSONArray items){
        LocalDevice localDevice = localDeviceMap.get(this.clientId);
        Map<Integer, JSONArray> groupItems = new HashMap<>();
        JSONObject joTmp;
        JSONArray jaTmp;
        for (int i = 0;i < items.size();i++){
            joTmp = items.getJSONObject(i);
            if(groupItems.containsKey(joTmp.getInteger("deviceId"))){
                jaTmp = groupItems.get(joTmp.getInteger("deviceId"));
                jaTmp.add(joTmp);
            }else{
                jaTmp = new JSONArray();
                jaTmp.add(joTmp);
                groupItems.put(joTmp.getInteger("deviceId"), jaTmp);
            }
        }
        Set<Integer> deviceIds = groupItems.keySet();
        for (Integer deviceId : deviceIds) {
            try {
                //获取某个远程设备
                RemoteDevice remoteDevice = localDevice.getRemoteDeviceBlocking(deviceId);
                JSONArray currentItems = groupItems.get(deviceId);
                for (int i = 0; i < currentItems.size(); i++) {
                    final JSONObject currentJo = currentItems.getJSONObject(i);
                    try {
                        Encodable value;
                        Object v = currentJo.get("value");
                        if(v instanceof java.lang.Boolean) {
                            value = currentJo.getBooleanValue("value") ? Boolean.TRUE : Boolean.FALSE;
                        }else if(v instanceof BigDecimal){
                            value = new Real(currentJo.getFloatValue("value"));
                        } else if(v instanceof Integer) {
                            value = new SignedInteger(currentJo.getIntValue("value"));
                        } else {
                            value = new EncodedValue(currentJo.getBytes("value"));
                        }
                        RequestUtils.writePresentValue(localDevice, remoteDevice, new ObjectIdentifier(currentJo.getIntValue("datatype"), currentJo.getIntValue("instance")), value);
                        results.put(currentJo.getString("tag"), currentJo.getString("value"));
                    }catch (Exception ex){
                        results.put(currentJo.getString("tag"), ex.getMessage());
                        logger.debug(ex.getMessage());
                    }
                }
            }catch (Exception e){
                logger.debug(e.getMessage());
            }
        }
    }

    @Override
    public PluginResultEntity execute(JSONObject jo) {
        waiting();
        PluginResultEntity pluginResultEntity = new PluginResultEntity();
        LocalDevice localDevice = localDeviceMap.get(this.clientId);
        List<RemoteDevice> remoteDevices = localDevice.getRemoteDevices();
        JSONArray ja = new JSONArray();
        Set<Integer> integers = localDeviceMap.keySet();
        for (RemoteDevice rd : remoteDevices) {
            if(!integers.contains(rd.getInstanceNumber())) {
                JSONObject tjo = new JSONObject();
                tjo.put("deviceId", rd.getInstanceNumber());
                tjo.put("info", rd.toString());
                try {
                    RemoteDevice remoteDevice = localDevice.getRemoteDeviceBlocking(rd.getInstanceNumber());
                    List<ObjectIdentifier> objectList = RequestUtils.getObjectList(localDevice, remoteDevice).getValues();
                    JSONArray jaT = new JSONArray();
                    for (ObjectIdentifier o : objectList) {
                        jaT.add(o.toString());
                    }
                    tjo.put("objects", jaT);
                }catch (Exception e){ }
                ja.add(tjo);
            }
        }
        pluginResultEntity.setContext(ja);
        pluginResultEntity.setOk(true);
        return pluginResultEntity;
    }
}
